package com.googlecode.tidej.shared.idl;

import com.googlecode.tidej.shared.model.Library;
import com.googlecode.tidej.shared.model.Model;
import com.googlecode.tidej.shared.model.Operation;
import com.googlecode.tidej.shared.model.Parameter;
import com.googlecode.tidej.shared.model.Property;
import com.googlecode.tidej.shared.model.Type;
import com.googlecode.tidej.shared.util.Tokenizer;

public class IdlParser {
  Tokenizer tokenizer;
  Model model;
  Library lib;
  
  public IdlParser(Model model, Library lib, String idl) {
    this.model = model;
    this.lib = lib;
    tokenizer = new Tokenizer(idl);
  }
 
  private void parseInterface(Operation constructor) {
    consume(Tokenizer.TT_WORD, "interface");
    Type type;
    if (tokenizer.ttype == '(' && constructor == null) {
      consume('(');
      consume(Tokenizer.TT_WORD, "Global");
      consume(')');
      type = lib.getGlobals();
    } else {
      String name = tokenizer.sval;
      tokenizer.nextToken();
      type = lib.getType(name);
      if (type == null) {
        type = new Type(name);
        lib.addType(type);
      }
      if (constructor != null) {
        constructor.setName(name);
        type.setConstructor(constructor);
      }
    }
    
    if (tokenizer.ttype == ':') {
      tokenizer.nextToken();
      Type superType = parseType();
      type.setSuperType(superType);
    }
    
    if (tokenizer.ttype == ';') {
      tokenizer.nextToken();
      return;
    }
    
    consume('{');
    while (tokenizer.ttype != '}') {
      String sval = tokenizer.sval;
      Type owner = type;
      if ("static".equals(tokenizer.sval)) {
        consumeIdentifier();
        owner = owner.getMetaType();
      }
      if ("readonly".equals(sval) || "attribute".equals(sval)) {
        Property property = parseProperty();
        owner.addProperty(property);
      } else if ("const".equals(sval)) {
        type.getMetaType().addProperty(parseConst());
      } else {
        Operation operation = parseOperation();
        Operation old = owner.getOperation(operation.getName());
        if (old != null) {
          old.merge(model, operation);
        } else {
          owner.addOperation(operation);
        }
      }
    }
    consume('}');
    consume(';');
  }
  
  private Property parseConst() {
    consume(Tokenizer.TT_WORD, "const");
    Type type = parseType();
    String name = consumeIdentifier();
    consume('=');
    StringBuilder sb = new StringBuilder();
    while(tokenizer.ttype != ';' && tokenizer.ttype != Tokenizer.TT_EOF) {
      sb.append(tokenizer.sval);
      sb.append(' ');
      tokenizer.nextToken();
    }
    consume(';');
    return new Property(name, type, true, sb.toString().trim());
  }

  private Property parseProperty() {
    if (tokenizer.sval.equals("readonly")) {
      tokenizer.nextToken();
    }
    consume(Tokenizer.TT_WORD, "attribute");
    Type propertyType = parseType();
    
    String name = tokenizer.sval;
    consume(Tokenizer.TT_WORD);
    consume(';');
    return new Property(name, propertyType, false, null);
  }
 
  private Operation parseOperation() {
    String name = null;
    boolean ommitable = "omittable".equals(tokenizer.sval);
    if (ommitable) {
      consumeIdentifier();
    }

    if ("getter".equals(tokenizer.sval) || "setter".equals(tokenizer.sval)) {
      if (ommitable) {
        consumeIdentifier();
      } else {
        name = consumeIdentifier();
      }
    }
    Type type;
    if ("void".equals(tokenizer.sval)) {
      type = null;
      consumeIdentifier();
    } else {
      type = parseType();
    }
    if (name == null) {
      name = consumeIdentifier();
    }
    Operation op = new Operation(name, type);
    parseParameterList(op);
    consume(';');
    return op;
  }
  
  private void fail(String msg) {
    tokenizer.fail("Error parsing " + lib.getName() + ": " + msg);
  }
  
  private Type parseType() {
    String name;
    while (true) {
      if (tokenizer.sval.equals("unsigned")) {
        consumeIdentifier();
        name = "unsigned " + consumeIdentifier();
      } else {
        name = consumeIdentifier();
      }
      if (tokenizer.ttype != Tokenizer.TT_SCOPE) {
        break;
      }
      tokenizer.nextToken();
    }
    
    if ("long".equals(tokenizer.sval) && name.endsWith("long")) {
      name += " long";
      consumeIdentifier();
    }
    
    boolean mayAddToLib = false;
    if (tokenizer.ttype == '[') {
      tokenizer.nextToken();
      consume(']');
      name += "[]";
    } else if (tokenizer.ttype == '<') {
      tokenizer.nextToken();
      Type elementType = parseType();
      name += '<' + elementType.getName() + '>';
      consume('>');
    } else {
      mayAddToLib = true;
    }
    Type type = lib.getType(name);
    if (type == null) {
      type = model.getType(name);
      if (type == null) {
        type = new Type(name);
        if (mayAddToLib) {
          lib.addType(type);
        } else {
          model.addHiddenType(type);
        }
      }
    }
    return type;
  }
  
  private void consume(int type) {
    if (type != tokenizer.ttype) {
      fail("Expected type: " + tokenizer.ttypeToString(type));
    }
    tokenizer.nextToken();
  }
    
  private void consume(int type, String sval) {
    if (!sval.equals(tokenizer.sval)) {
      fail("Expected: " + sval);
    }
    consume(type);
  }

  private String consumeIdentifier() {
    String s = tokenizer.sval;
    consume(Tokenizer.TT_WORD);
    return s;
  }

  public void parse() {
    tokenizer.nextToken();
    parseModuleBody();
    consume(Tokenizer.TT_EOF);
  }
  
  public void parseModuleBody() {
    while(tokenizer.ttype != Tokenizer.TT_EOF && tokenizer.ttype != '}') {
      String sval = tokenizer.sval;
      if (tokenizer.ttype == '[') {
        Operation constructor = parseConstructors();
        parseInterface(constructor);
      } else if ("interface".equals(sval)) {
        parseInterface(null);
      } else if ("typedef".equals(sval)) {
        parseTypedef();
      } else if ("valuetype".equals(sval)) {
        parseValueType();
      } else if ("module".equals(sval)) {
        parseModule();
      } else if ("const".equals(sval)) {
        lib.getGlobals().addProperty(parseConst());
      } else if ("exception".equals(sval)) {
        parseException();
      } else {
        fail("exception, interface, typedef, valuetype, module or const expected");
      }
    }
  }
  
  private void parseException() {
    consume(Tokenizer.TT_WORD, "exception");
    String name = consumeIdentifier();
    Type type = new Type(name);
    lib.addType(type);
    consume('{');
    
    while(tokenizer.ttype != '}' && tokenizer.ttype != Tokenizer.TT_EOF) {
      Type pType = parseType();
      String pName = consumeIdentifier();
      type.addProperty(new Property(pName, pType, false, null));
      consume(';');
    }
    consume('}');
    consume(';');
  }

  private Operation parseConstructors() {
    consume('[');
    Operation result = null;
    while("Constructor".equals(tokenizer.sval)) {
      consumeIdentifier();
      Operation c = new Operation("", null);
      parseParameterList(c);
      if (result == null) {
        result = c;
      } else {
        result.merge(model, c);
      }
      if (tokenizer.ttype != ',') {
        break;
      }
      consume(',');
    }
    consume(']');
    return result;
  }

  private void parseModule() {
    consume(Tokenizer.TT_WORD, "module");
    // name
    consumeIdentifier();
    consume('{');
    parseModuleBody();
    consume('}');
    consume(';');
  }

  private void parseParameterList(Operation op) {
    consume('(');
    while(tokenizer.ttype != ')') {
      int modifiers = 0;
      if ("optional".equals(tokenizer.sval)) {
        modifiers = Parameter.OPTIONAL;
        consumeIdentifier();
      }
      if ("in".equals(tokenizer.sval) || 
          "out".equals(tokenizer.sval) ||
          "inout".equals(tokenizer.sval)) {
        consumeIdentifier();
      }
      Type pType = parseType();
      if (tokenizer.ttype == Tokenizer.TT_ELLIPSIS) {
        modifiers |= Parameter.VARIADIC;
        tokenizer.nextToken();
      }
      String pName = consumeIdentifier();
      Parameter parameter = new Parameter(pName, pType, modifiers);
      op.addParameter(parameter);
      if(tokenizer.ttype == ',') {
        tokenizer.nextToken();
      } else if (tokenizer.ttype != ')') {
        fail("',' or ')' expected");
      }
    }
    consume(')');
    if ("raises".equals(tokenizer.sval)) {
      consumeIdentifier();
      consume('(');
      parseType();
      consume(')');
    }
  }
  
  private void parseTypedef() {
    consume(Tokenizer.TT_WORD, "typedef");
    Type oldType = parseType();
    Type newType = parseType();
    if (oldType != newType) {
      newType.setSuperType(oldType);
    }
    consume(';');
  }

  private void parseValueType() {
    consume(Tokenizer.TT_WORD, "valuetype");
    Type newType = parseType();
    Type oldType = parseType();
    if (oldType != newType) {
      newType.setSuperType(oldType);
    }
    consume(';');
  }

}